// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)

package org.xbill.DNS;

import java.net.*;
import java.net.Inet6Address;

/**
 * Routines dealing with IP addresses. Includes functions similar to those in
 * the java.net.InetAddress class.
 * 
 * @author Brian Wellington
 */

public final class Address {

	public static final int IPv4 = 1;
	public static final int IPv6 = 2;

	private Address() {
	}

	private static byte[] parseV4(String s) {
		int numDigits;
		int currentOctet;
		byte[] values = new byte[4];
		int currentValue;
		int length = s.length();

		currentOctet = 0;
		currentValue = 0;
		numDigits = 0;
		for (int i = 0; i < length; i++) {
			char c = s.charAt(i);
			if (c >= '0' && c <= '9') {
				/* Can't have more than 3 digits per octet. */
				if (numDigits == 3)
					return null;
				/* Octets shouldn't start with 0, unless they are 0. */
				if (numDigits > 0 && currentValue == 0)
					return null;
				numDigits++;
				currentValue *= 10;
				currentValue += (c - '0');
				/* 255 is the maximum value for an octet. */
				if (currentValue > 255)
					return null;
			} else if (c == '.') {
				/* Can't have more than 3 dots. */
				if (currentOctet == 3)
					return null;
				/* Two consecutive dots are bad. */
				if (numDigits == 0)
					return null;
				values[currentOctet++] = (byte) currentValue;
				currentValue = 0;
				numDigits = 0;
			} else
				return null;
		}
		/* Must have 4 octets. */
		if (currentOctet != 3)
			return null;
		/* The fourth octet can't be empty. */
		if (numDigits == 0)
			return null;
		values[currentOctet] = (byte) currentValue;
		return values;
	}

	private static byte[] parseV6(String s) {
		int range = -1;
		byte[] data = new byte[16];

		String[] tokens = s.split(":", -1);

		int first = 0;
		int last = tokens.length - 1;

		if (tokens[0].length() == 0) {
			// If the first two tokens are empty, it means the string
			// started with ::, which is fine. If only the first is
			// empty, the string started with :, which is bad.
			if (last - first > 0 && tokens[1].length() == 0)
				first++;
			else
				return null;
		}

		if (tokens[last].length() == 0) {
			// If the last two tokens are empty, it means the string
			// ended with ::, which is fine. If only the last is
			// empty, the string ended with :, which is bad.
			if (last - first > 0 && tokens[last - 1].length() == 0)
				last--;
			else
				return null;
		}

		if (last - first + 1 > 8)
			return null;

		int i, j;
		for (i = first, j = 0; i <= last; i++) {
			if (tokens[i].length() == 0) {
				if (range >= 0)
					return null;
				range = j;
				continue;
			}

			if (tokens[i].indexOf('.') >= 0) {
				// An IPv4 address must be the last component
				if (i < last)
					return null;
				// There can't have been more than 6 components.
				if (i > 6)
					return null;
				byte[] v4addr = Address.toByteArray(tokens[i], IPv4);
				if (v4addr == null)
					return null;
				for (int k = 0; k < 4; k++)
					data[j++] = v4addr[k];
				break;
			}

			try {
				for (int k = 0; k < tokens[i].length(); k++) {
					char c = tokens[i].charAt(k);
					if (Character.digit(c, 16) < 0)
						return null;
				}
				int x = Integer.parseInt(tokens[i], 16);
				if (x > 0xFFFF || x < 0)
					return null;
				data[j++] = (byte) (x >>> 8);
				data[j++] = (byte) (x & 0xFF);
			} catch (NumberFormatException e) {
				return null;
			}
		}

		if (j < 16 && range < 0)
			return null;

		if (range >= 0) {
			int empty = 16 - j;
			System.arraycopy(data, range, data, range + empty, j - range);
			for (i = range; i < range + empty; i++)
				data[i] = 0;
		}

		return data;
	}

	/**
	 * Convert a string containing an IP address to an array of 4 or 16
	 * integers.
	 * 
	 * @param s
	 *            The address, in text format.
	 * @param family
	 *            The address family.
	 * @return The address
	 */
	public static int[] toArray(String s, int family) {
		byte[] byteArray = toByteArray(s, family);
		if (byteArray == null)
			return null;
		int[] intArray = new int[byteArray.length];
		for (int i = 0; i < byteArray.length; i++)
			intArray[i] = byteArray[i] & 0xFF;
		return intArray;
	}

	/**
	 * Convert a string containing an IPv4 address to an array of 4 integers.
	 * 
	 * @param s
	 *            The address, in text format.
	 * @return The address
	 */
	public static int[] toArray(String s) {
		return toArray(s, IPv4);
	}

	/**
	 * Convert a string containing an IP address to an array of 4 or 16 bytes.
	 * 
	 * @param s
	 *            The address, in text format.
	 * @param family
	 *            The address family.
	 * @return The address
	 */
	public static byte[] toByteArray(String s, int family) {
		if (family == IPv4)
			return parseV4(s);
		else if (family == IPv6)
			return parseV6(s);
		else
			throw new IllegalArgumentException("unknown address family");
	}

	/**
	 * Determines if a string contains a valid IP address.
	 * 
	 * @param s
	 *            The string
	 * @return Whether the string contains a valid IP address
	 */
	public static boolean isDottedQuad(String s) {
		byte[] address = Address.toByteArray(s, IPv4);
		return (address != null);
	}

	/**
	 * Converts a byte array containing an IPv4 address into a dotted quad
	 * string.
	 * 
	 * @param addr
	 *            The array
	 * @return The string representation
	 */
	public static String toDottedQuad(byte[] addr) {
		return ((addr[0] & 0xFF) + "." + (addr[1] & 0xFF) + "."
				+ (addr[2] & 0xFF) + "." + (addr[3] & 0xFF));
	}

	/**
	 * Converts an int array containing an IPv4 address into a dotted quad
	 * string.
	 * 
	 * @param addr
	 *            The array
	 * @return The string representation
	 */
	public static String toDottedQuad(int[] addr) {
		return (addr[0] + "." + addr[1] + "." + addr[2] + "." + addr[3]);
	}

	private static Record[] lookupHostName(String name)
			throws UnknownHostException {
		try {
			Record[] records = new Lookup(name).run();
			if (records == null)
				throw new UnknownHostException("unknown host");
			return records;
		} catch (TextParseException e) {
			throw new UnknownHostException("invalid name");
		}
	}

	private static InetAddress addrFromRecord(String name, Record r)
			throws UnknownHostException {
		ARecord a = (ARecord) r;
		return InetAddress.getByAddress(name, a.getAddress().getAddress());
	}

	/**
	 * Determines the IP address of a host
	 * 
	 * @param name
	 *            The hostname to look up
	 * @return The first matching IP address
	 * @exception UnknownHostException
	 *                The hostname does not have any addresses
	 */
	public static InetAddress getByName(String name)
			throws UnknownHostException {
		try {
			return getByAddress(name);
		} catch (UnknownHostException e) {
			Record[] records = lookupHostName(name);
			return addrFromRecord(name, records[0]);
		}
	}

	/**
	 * Determines all IP address of a host
	 * 
	 * @param name
	 *            The hostname to look up
	 * @return All matching IP addresses
	 * @exception UnknownHostException
	 *                The hostname does not have any addresses
	 */
	public static InetAddress[] getAllByName(String name)
			throws UnknownHostException {
		try {
			InetAddress addr = getByAddress(name);
			return new InetAddress[] { addr };
		} catch (UnknownHostException e) {
			Record[] records = lookupHostName(name);
			InetAddress[] addrs = new InetAddress[records.length];
			for (int i = 0; i < records.length; i++)
				addrs[i] = addrFromRecord(name, records[i]);
			return addrs;
		}
	}

	/**
	 * Converts an address from its string representation to an IP address. The
	 * address can be either IPv4 or IPv6.
	 * 
	 * @param addr
	 *            The address, in string form
	 * @return The IP addresses
	 * @exception UnknownHostException
	 *                The address is not a valid IP address.
	 */
	public static InetAddress getByAddress(String addr)
			throws UnknownHostException {
		byte[] bytes;
		bytes = toByteArray(addr, IPv4);
		if (bytes != null)
			return InetAddress.getByAddress(addr, bytes);
		bytes = toByteArray(addr, IPv6);
		if (bytes != null)
			return InetAddress.getByAddress(addr, bytes);
		throw new UnknownHostException("Invalid address: " + addr);
	}

	/**
	 * Converts an address from its string representation to an IP address in a
	 * particular family.
	 * 
	 * @param addr
	 *            The address, in string form
	 * @param family
	 *            The address family, either IPv4 or IPv6.
	 * @return The IP addresses
	 * @exception UnknownHostException
	 *                The address is not a valid IP address in the specified
	 *                address family.
	 */
	public static InetAddress getByAddress(String addr, int family)
			throws UnknownHostException {
		if (family != IPv4 && family != IPv6)
			throw new IllegalArgumentException("unknown address family");
		byte[] bytes;
		bytes = toByteArray(addr, family);
		if (bytes != null)
			return InetAddress.getByAddress(addr, bytes);
		throw new UnknownHostException("Invalid address: " + addr);
	}

	/**
	 * Determines the hostname for an address
	 * 
	 * @param addr
	 *            The address to look up
	 * @return The associated host name
	 * @exception UnknownHostException
	 *                There is no hostname for the address
	 */
	public static String getHostName(InetAddress addr)
			throws UnknownHostException {
		Name name = ReverseMap.fromAddress(addr);
		Record[] records = new Lookup(name, Type.PTR).run();
		if (records == null)
			throw new UnknownHostException("unknown address");
		PTRRecord ptr = (PTRRecord) records[0];
		return ptr.getTarget().toString();
	}

	/**
	 * Returns the family of an InetAddress.
	 * 
	 * @param address
	 *            The supplied address.
	 * @return The family, either IPv4 or IPv6.
	 */
	public static int familyOf(InetAddress address) {
		if (address instanceof Inet4Address)
			return IPv4;
		if (address instanceof Inet6Address)
			return IPv6;
		throw new IllegalArgumentException("unknown address family");
	}

	/**
	 * Returns the length of an address in a particular family.
	 * 
	 * @param family
	 *            The address family, either IPv4 or IPv6.
	 * @return The length of addresses in that family.
	 */
	public static int addressLength(int family) {
		if (family == IPv4)
			return 4;
		if (family == IPv6)
			return 16;
		throw new IllegalArgumentException("unknown address family");
	}

	/**
	 * Truncates an address to the specified number of bits. For example,
	 * truncating the address 10.1.2.3 to 8 bits would yield 10.0.0.0.
	 * 
	 * @param address
	 *            The source address
	 * @param maskLength
	 *            The number of bits to truncate the address to.
	 */
	public static InetAddress truncate(InetAddress address, int maskLength) {
		int family = familyOf(address);
		int maxMaskLength = addressLength(family) * 8;
		if (maskLength < 0 || maskLength > maxMaskLength)
			throw new IllegalArgumentException("invalid mask length");
		if (maskLength == maxMaskLength)
			return address;
		byte[] bytes = address.getAddress();
		for (int i = maskLength / 8 + 1; i < bytes.length; i++)
			bytes[i] = 0;
		int maskBits = maskLength % 8;
		int bitmask = 0;
		for (int i = 0; i < maskBits; i++)
			bitmask |= (1 << (7 - i));
		bytes[maskLength / 8] &= bitmask;
		try {
			return InetAddress.getByAddress(bytes);
		} catch (UnknownHostException e) {
			throw new IllegalArgumentException("invalid address");
		}
	}

}
